/* eslint-disable @typescript-eslint/no-explicit-any */
import { Component, cls, type StyledProps } from '@knno/jsx';
import SVGDrag from '@mdi/svg/svg/drag.svg';
import { knnoTable } from '../../theme/table.css';
import { isDescendant, size2px } from '../../tools/utils';
import type { DateMode } from '../datePicker/datePicker';
import { defineElement } from '../defineElement';
import {
	activeTableCell,
	findEventElement,
	findParent,
	getTableActiveCellPos,
	isCursorAtBegin,
	isCursorAtEnd,
} from '../editor';
import { genWidth } from '../gridBase';
import { Icon } from '../icon/icon';
import type { SelectType } from '../list/list';
import type { PopupPosition } from '../popup/popup';
import { DATA } from '../symbol';
import type { TagValue } from '../tag/tag';
import { CheckCell, type CheckOption } from './controls/check';
import { ComboCell, type ComboValue } from './controls/combo';
import { DateCell } from './controls/date';
import { NumberCell } from './controls/number';
import { SelectCell, type SelectOptionInfo, type SelectOptions } from './controls/select';
import { TagCell } from './controls/tag';
import { TextCell } from './controls/text';
import type { IconType, InputBase, TextAlign } from './inputBase';
import { ANI_TIME, inputGridStyle } from './inputGrid.css';

export * from './controls/check';
export * from './controls/combo';
export * from './controls/date';
export * from './controls/number';
export * from './controls/select';
export * from './controls/tag';
export * from './controls/text';

function canGoLeft(cell: HTMLElement) {
	if (cell.isContentEditable) {
		return isCursorAtBegin(cell);
	} else {
		return (
			document.activeElement === cell ||
			(cell.firstElementChild
				? isDescendant([cell.firstElementChild], document.activeElement)
				: false)
		);
	}
}

function canGoUp(cell: HTMLElement) {
	if (cell.classList.contains('popup-open')) return false;
	if (cell.isContentEditable) {
		return isCursorAtBegin(cell);
	} else {
		return true;
	}
}

function canGoRight(cell: HTMLElement) {
	if (cell.isContentEditable) {
		return isCursorAtEnd(cell);
	} else {
		return (
			document.activeElement === cell ||
			(cell.lastElementChild
				? isDescendant([cell.lastElementChild], document.activeElement)
				: false)
		);
	}
}

function canGoDown(cell: HTMLElement) {
	if (cell.classList.contains('popup-open')) return false;
	if (cell.isContentEditable) {
		return isCursorAtEnd(cell);
	} else {
		return true;
	}
}

function getRowNum(tr: HTMLTableRowElement): number {
	let index = 0;
	let currentElement: Element = tr;
	while (currentElement.previousElementSibling) {
		currentElement = currentElement.previousElementSibling;
		if (['line', 'reorderable'].every((s) => currentElement.classList.contains(s))) {
			index++;
		}
	}
	return index;
}

type RowInfo = { rowIndex: number; row: HTMLTableRowElement; rect: DOMRect };

type DragContext = {
	table: HTMLTableElement;
	mouseDownTableTop: number;
	mouseDownPos: { x: number; y: number };
	mousePos: { x: number; y: number };
	dragRow: RowInfo;
	targetRow?: RowInfo;
	tableRows: RowInfo[];
	// infoDiv?: HTMLDivElement;
	over: boolean;
};

function renderDrag(context: DragContext) {
	if (context.over) {
		// context.infoDiv?.remove();
		return;
	}
	// if (!context.infoDiv) {
	// 	context.infoDiv = document.createElement('div');
	// 	context.infoDiv.style.position = 'absolute';
	// 	context.infoDiv.style.width = '400px';
	// 	context.infoDiv.style.backgroundColor = 'white';
	// 	context.infoDiv.style.color = 'black';
	// 	context.infoDiv.style.padding = '10px';
	// 	context.infoDiv.style.left = '0px';
	// 	context.infoDiv.style.top = '0px';
	// 	document.body.appendChild(context.infoDiv);
	// }
	// let infoText = '';
	let left = context.mousePos.x - context.mouseDownPos.x;
	if (left < -4) left = -4;
	if (left > 4) left = 4;
	const tableTop = context.table.getBoundingClientRect().top;
	const tableMoved = tableTop - context.mouseDownTableTop;
	context.dragRow.row.style.translate =
		left + 'px ' + (context.mousePos.y - context.mouseDownPos.y - tableMoved) + 'px';
	const y = context.mousePos.y - tableMoved;
	// infoText = `y: ${y}`;
	const targetRowInfo = context.tableRows.find((row) => {
		const rect = row.rect;
		return y > rect.top && y < rect.top + rect.height;
	});
	// infoText += `\npos: ${targetRowInfo?.rowIndex}`;
	if (targetRowInfo && targetRowInfo.rowIndex !== context.targetRow?.rowIndex) {
		context.targetRow = targetRowInfo;
		for (let j = 0; j < context.tableRows.length; j++) {
			const rowInfo = context.tableRows[j];
			if (j >= targetRowInfo.rowIndex && j < context.dragRow.rowIndex) {
				rowInfo.row.style.translate = '0 ' + context.dragRow.rect.height + 'px';
			} else if (j <= targetRowInfo.rowIndex && j > context.dragRow.rowIndex) {
				rowInfo.row.style.translate = '0 ' + -context.dragRow.rect.height + 'px';
			} else if (j !== context.dragRow.rowIndex) {
				rowInfo.row.style.translate = '';
			}
		}
		targetRowInfo.row.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
	}
	// infoText += `\ntarget: ${context.targetRow?.rowIndex}`;
	// context.infoDiv.innerHTML = infoText;
	requestAnimationFrame(() => {
		renderDrag(context);
	});
}

const InputGridElement = defineElement('knno-grid-input', HTMLTableElement, 'table');

const MIN_WIDTH = 10;

export type ColumnType = 'text' | 'number' | 'select' | 'check' | 'date' | 'combo' | 'tag';

export type InputGridColumn = {
	type: ColumnType;
	field: string;
	title: string;
	textAlign?: TextAlign;
	readOnly?: boolean;
	optional?: boolean;
	icon?: IconType;
	width?: string | number;
} & (
	| {
			type: 'text';
			multiline?: boolean;
			maxLength?: number;
			minLength?: number;
			pattern?: RegExp;
			defaultValue?: string;
			validation?: (this: TextCell) => string;
	  }
	| {
			type: 'number';
			maxPrecision?: number;
			minPrecision?: number;
			prefix?: string;
			suffix?: string;
			thousandsComma?: boolean;
			max?: number;
			min?: number;
			defaultValue?: number;
			validation?: (this: NumberCell) => string;
	  }
	| {
			type: 'check';
			options: CheckOption[];
			minCount?: number;
			maxCount?: number;
			radio?: boolean;
			defaultValue?: CheckOption[];
			validation?: (this: CheckCell) => string;
	  }
	| {
			type: 'select';
			multiple?: boolean;
			multiSelectType?: SelectType;
			searchBox?: boolean;
			popupWidth?: number | string;
			returnFullPath?: boolean | string;
			options?: SelectOptions;
			minCount?: number;
			maxCount?: number;
			defaultValue?: SelectOptionInfo[];
			validation?: (this: SelectCell) => string;
	  }
	| {
			type: 'combo';
			queryHandler?: (keyword: string) => Promise<ComboValue[]>;
			multiline?: undefined;
			popupWidth?: string | number;
			popupPosition?: PopupPosition;
			defaultValue?: ComboValue;
			validation?: (this: ComboCell) => string;
	  }
	| {
			type: 'date';
			min?: string;
			max?: string;
			mode?: DateMode;
			format?: string;
			defaultValue?: string;
			validation?: (this: DateCell) => string;
	  }
	| {
			type: 'tag';
			options?: TagValue[];
			minCount?: number;
			maxCount?: number;
			selectOnly?: boolean;
			defaultValue?: TagValue[];
			validation?: (this: TagCell) => string;
	  }
);

export type SummaryColumn<C extends readonly InputGridColumn[]> = {
	text: string | ((this: InputGrid<C>) => string);
	colSpan?: number;
	textAlign?: TextAlign;
};

type RenderCellProc<C extends InputGridColumn[], T extends InputGridColumn> = (
	this: InputGrid<C>,
	column: T,
	row: DataFromColumns<C>,
	onChange: (value: unknown) => void,
) => Node;

function invalidColumn(): never {
	throw new Error('Invalid column type');
}

const PROCS: Record<ColumnType, RenderCellProc<any, any>> = {
	text(
		this: InputGrid<any>,
		column: InputGridColumn,
		row: Record<string, any>,
		onChange: (value: unknown) => void,
	): Node {
		if (column.type !== 'text') {
			invalidColumn();
		}
		const { width: _, ...others } = column;
		return (
			<TextCell
				{...others}
				readOnly={this.readOnly}
				class={column.readOnly ? 'read-only' : ''}
				value={row[column.field] ?? column.defaultValue}
				validation={column.validation}
				onChange={function () {
					onChange(this.value);
				}}
				ref={(cell) => {
					if (row[column.field] === undefined && column.defaultValue !== undefined) {
						row[column.field] = column.defaultValue;
					}
					this[DATA].set(cell.el, cell);
				}}
			/>
		);
	},
	number(
		this: InputGrid<any>,
		column: InputGridColumn,
		row: Record<string, any>,
		onChange: (value: unknown) => void,
	): Node {
		if (column.type !== 'number') {
			invalidColumn();
		}
		const { width: _, ...others } = column;
		return (
			<NumberCell
				{...others}
				readOnly={this.readOnly}
				class={column.readOnly ? 'read-only' : ''}
				value={row[column.field] ?? column.defaultValue}
				validation={column.validation}
				onChange={function () {
					onChange(this.value);
				}}
				ref={(cell) => {
					if (row[column.field] === undefined && column.defaultValue !== undefined) {
						row[column.field] = column.defaultValue;
					}
					this[DATA].set(cell.el, cell);
				}}
			/>
		);
	},
	check(
		this: InputGrid<any>,
		column: InputGridColumn,
		row: Record<string, any>,
		onChange: (value: unknown) => void,
	): Node {
		if (column.type !== 'check') {
			invalidColumn();
		}
		const { width: _, ...others } = column;
		return (
			<CheckCell
				{...others}
				readOnly={this.readOnly}
				class={column.readOnly ? 'read-only' : ''}
				value={row[column.field] ?? column.defaultValue}
				validation={column.validation}
				onChange={function () {
					onChange(this.value);
				}}
				ref={(cell) => {
					if (row[column.field] === undefined && column.defaultValue !== undefined) {
						row[column.field] = column.defaultValue;
					}
					this[DATA].set(cell.el, cell);
				}}
			/>
		);
	},
	select(
		this: InputGrid<any>,
		column: InputGridColumn,
		row: Record<string, any>,
		onChange: (value: unknown) => void,
	): Node {
		if (column.type !== 'select') {
			invalidColumn();
		}
		const { width: _, ...others } = column;
		return (
			<SelectCell
				{...others}
				readOnly={this.readOnly}
				class={column.readOnly ? 'read-only' : ''}
				value={row[column.field] ?? column.defaultValue}
				validation={column.validation}
				onChange={function () {
					onChange(this.value);
				}}
				ref={(cell) => {
					if (row[column.field] === undefined && column.defaultValue !== undefined) {
						row[column.field] = column.defaultValue;
					}
					this[DATA].set(cell.el, cell);
				}}
			/>
		);
	},
	combo(
		this: InputGrid<any>,
		column: InputGridColumn,
		row: Record<string, any>,
		onChange: (value: unknown) => void,
	): Node {
		if (column.type !== 'combo') {
			invalidColumn();
		}
		const { width: _, ...others } = column;
		return (
			<ComboCell
				{...others}
				readOnly={this.readOnly}
				class={column.readOnly ? 'read-only' : ''}
				value={row[column.field] ?? column.defaultValue}
				validation={column.validation}
				onChange={function () {
					onChange(this.value);
				}}
				ref={(cell) => {
					if (row[column.field] === undefined && column.defaultValue !== undefined) {
						row[column.field] = column.defaultValue;
					}
					this[DATA].set(cell.el, cell);
				}}
			/>
		);
	},
	date(
		this: InputGrid<any>,
		column: InputGridColumn,
		row: Record<string, any>,
		onChange: (value: unknown) => void,
	): Node {
		if (column.type !== 'date') {
			invalidColumn();
		}
		const { width: _, ...others } = column;
		return (
			<DateCell
				{...others}
				readOnly={this.readOnly}
				class={column.readOnly ? 'read-only' : ''}
				value={row[column.field] ?? column.defaultValue}
				validation={column.validation}
				onChange={function () {
					onChange(this.value);
				}}
				ref={(cell) => {
					if (row[column.field] === undefined && column.defaultValue !== undefined) {
						row[column.field] = column.defaultValue;
					}
					this[DATA].set(cell.el, cell);
				}}
			/>
		);
	},
	tag(
		this: InputGrid<any>,
		column: InputGridColumn,
		row: Record<string, any>,
		onChange: (value: unknown) => void,
	): Node {
		if (column.type !== 'tag') {
			invalidColumn();
		}
		const { width: _, ...others } = column;
		return (
			<TagCell
				{...others}
				readOnly={this.readOnly}
				class={column.readOnly ? 'read-only' : ''}
				value={row[column.field] ?? column.defaultValue}
				validation={column.validation}
				onChange={function () {
					onChange(this.value);
				}}
				ref={(cell) => {
					if (row[column.field] === undefined && column.defaultValue !== undefined) {
						row[column.field] = column.defaultValue;
					}
					this[DATA].set(cell.el, cell);
				}}
			/>
		);
	},
};

type DataFromColumns<T extends readonly InputGridColumn[]> = {
	[K in T[number] as K['field']]?: K['type'] extends 'text'
		? string
		: K['type'] extends 'number'
			? number
			: K['type'] extends 'check'
				? (string | number | boolean)[]
				: K['type'] extends 'select'
					? { value: any; text: string }[]
					: K['type'] extends 'tag'
						? string[]
						: K['type'] extends 'combo'
							? string
							: K['type'] extends 'date'
								? string
								: never;
};

export type InputGridProps<T extends readonly InputGridColumn[]> = StyledProps<InputGrid<T>> & {
	columns: T;
	summary?: SummaryColumn<T>[];
	serialNo?: boolean;
	readOnly?: boolean;
	disableRowAdd?: boolean;
	disableRowDelete?: boolean;
	reorderable?: boolean;
	data?: DataFromColumns<T>[];
	onChange?: (this: InputGrid<T>) => void;
	onRowAdd?: (this: InputGrid<T>, row: Record<string, any>, rowIndex: number) => void;
	onRowDelete?: (this: InputGrid<T>, row: Record<string, any>, rowIndex: number) => void;
};

export class InputGrid<T extends readonly InputGridColumn[]> extends Component<
	InputGridProps<T>,
	InputGrid<T>,
	HTMLTableElement
> {
	private [DATA] = new WeakMap<HTMLElement, InputBase<any, any, any, any>>();
	constructor(props: InputGridProps<T>) {
		super(props);
		// this.setColumns(props.columns);
		// this.setData(props.data ?? []);
		// this.setSerialNo(this.serialNo);
		this.setData(this.data);
	}
	protected render(): Node {
		return (
			<InputGridElement
				class={cls(this.props.class, inputGridStyle, knnoTable)}
				style={this.props.style}
				onConnected={() => {
					this.adjustColumnSize();
				}}
			>
				<colgroup></colgroup>
				<tbody></tbody>
			</InputGridElement>
		);
	}

	private renderRow(row: DataFromColumns<T>): HTMLTableRowElement {
		let context: DragContext | null = null;
		const tr = (
			<tr
				class={this.reorderable ? 'line reorderable' : 'line'}
				onKeyDownCapture={(evt) => {
					const cell = findEventElement(evt, 'TD');
					if (!cell) return;
					if (evt.ctrlKey && evt.key === 'Enter') {
						if (this.readOnly || this.disableRowAdd) return;
						evt.preventDefault();
						evt.stopImmediatePropagation();
						if (this.data.length === 0) {
							this.data.push({} as DataFromColumns<T>);
						}
						if (evt.shiftKey) {
							this.insertRowBefore();
						} else {
							this.insertRowAfter();
						}
						this.emitChange();
					} else if (evt.key === 'Backspace' && evt.ctrlKey) {
						if (this.readOnly || this.disableRowDelete) return;
						evt.preventDefault();
						evt.stopImmediatePropagation();
						this.deleteRow();
						this.emitChange();
					} else if (evt.key === 'ArrowUp') {
						if (
							evt.ctrlKey &&
							evt.shiftKey &&
							!this.readOnly &&
							this.reorderable &&
							this.data.length > 1
						) {
							const line = getRowNum(tr);
							this.rowMoveUp(line);
							evt.preventDefault();
							evt.stopImmediatePropagation();
						} else if (cell.classList.contains('read-only') || canGoUp(cell) || evt.ctrlKey) {
							evt.preventDefault();
							evt.stopImmediatePropagation();
							this.activePreviousRow();
						}
					} else if (evt.key === 'ArrowDown') {
						if (
							evt.ctrlKey &&
							evt.shiftKey &&
							!this.readOnly &&
							this.reorderable &&
							this.data.length > 1
						) {
							const line = getRowNum(tr);
							this.rowMoveDown(line);
							evt.preventDefault();
							evt.stopImmediatePropagation();
						} else if (cell.classList.contains('read-only') || canGoDown(cell) || evt.ctrlKey) {
							evt.preventDefault();
							evt.stopImmediatePropagation();
							this.activeNextRow();
						}
					} else if (evt.key === 'ArrowLeft') {
						if (evt.shiftKey) return;
						if (cell.classList.contains('read-only') || canGoLeft(cell) || evt.ctrlKey) {
							evt.preventDefault();
							evt.stopImmediatePropagation();
							this.activePreviousCell();
						}
					} else if (evt.key === 'ArrowRight') {
						if (evt.shiftKey) return;
						if (cell.classList.contains('read-only') || canGoRight(cell) || evt.ctrlKey) {
							evt.preventDefault();
							evt.stopImmediatePropagation();
							this.activeNextCell();
						}
					}
				}}
			>
				{this.reorderable && !this.readOnly ? (
					<td
						style={{ padding: 0, textAlign: 'center', userSelect: 'none' }}
						onPointerDown={function (evt) {
							if (evt.button === 0) {
								tr.classList.add('dragging');
								const tableRows = Array.from(tr.parentElement?.children || [])
									.filter(
										(item) =>
											item instanceof HTMLTableRowElement && item.classList.contains('reorderable'),
									)
									.map((item, idx) => {
										return {
											row: item as HTMLTableRowElement,
											rect: item.getBoundingClientRect(),
											rowIndex: idx,
										};
									});
								const table = findParent(this, 'table') as HTMLTableElement;
								const line = getRowNum(tr);
								context = {
									table: table,
									mouseDownTableTop: table.getBoundingClientRect().top,
									mouseDownPos: { x: evt.clientX, y: evt.clientY },
									mousePos: { x: evt.clientX, y: evt.clientY },
									dragRow: { row: tr, rowIndex: line, rect: tr.getBoundingClientRect() },
									tableRows,
									over: false,
								};
								requestAnimationFrame(() => {
									renderDrag(context!);
								});
								this.setPointerCapture(evt.pointerId);
							}
						}}
						onPointerMove={(evt) => {
							if (context) {
								context.mousePos = { x: evt.clientX, y: evt.clientY };
							}
						}}
						ref={(td) => {
							td.addEventListener('pointerup', (evt) => {
								if (context) {
									const ctx = context;
									context = null;
									ctx.over = true;
									tr.classList.remove('dragging');
									tr.style.zIndex = '100';
									td.releasePointerCapture(evt.pointerId);
									if (ctx?.targetRow && ctx.dragRow.rowIndex !== ctx.targetRow.rowIndex) {
										const srcItem = this.data.splice(ctx.dragRow.rowIndex, 1);
										this.data.splice(ctx.targetRow.rowIndex, 0, srcItem[0]);
										if (ctx.targetRow.rowIndex > ctx.dragRow.rowIndex) {
											tr.style.translate =
												'0 ' +
												(ctx.targetRow.rect.top -
													ctx.dragRow.rect.top +
													ctx.targetRow.rect.height -
													ctx.dragRow.rect.height) +
												'px';
										} else {
											tr.style.translate =
												'0 ' + (ctx.targetRow.rect.top - ctx.dragRow.rect.top) + 'px';
										}
										this.emitChange();
										setTimeout(() => {
											ctx.tableRows.forEach((rowInfo) => {
												rowInfo.row.style.transition = 'none';
												rowInfo.row.style.translate = '';
												// rowInfo.row.classList.add('reorderable');
											});
											if (ctx.targetRow!.rowIndex > ctx.dragRow.rowIndex) {
												ctx.targetRow!.row.after(ctx.dragRow.row);
											} else {
												ctx.targetRow!.row.before(ctx.dragRow.row);
											}
											setTimeout(() => {
												ctx.tableRows.forEach((rowInfo) => {
													rowInfo.row.style.transition = '';
												});
												tr.style.zIndex = '';
											}, 32);
											// setTimeout(() => {
											// 	ctx.tableRows.forEach((rowInfo) => {
											// 		rowInfo.row.classList.add('reorderable');
											// 	});
											// }, 100);
											// this.renderContent();
										}, parseFloat(ANI_TIME) * 1000);
									} else {
										tr.style.translate = '';
									}
								}
							});
						}}
					>
						<Icon svg={SVGDrag} size="1.2em" />
					</td>
				) : (
					''
				)}
				{this.serialNo ? <td class="serial" style={{ textAlign: 'center' }}></td> : ''}
				{this.columns.map((column) =>
					PROCS[column.type].call(this, column, row, (value) => {
						row[column.field as keyof DataFromColumns<T>] = value as any;
						if (this.data.length === 0) {
							this.data.push(row);
						}
						this.emitChange();
					}),
				)}
			</tr>
		) as HTMLTableRowElement;
		return tr;
	}

	private renderContent() {
		const fragement = <></>;
		const headLine = (
			<tr>
				{this.reorderable && !this.readOnly ? <th></th> : ''}
				{this.serialNo ? <th>序号</th> : ''}
				{this.columns.map((column) => (
					<th>{column.title}</th>
				))}
			</tr>
		);
		fragement.appendChild(headLine);

		const data = this.data.concat(
			!this.readOnly && !this.disableRowAdd && this.data.length === 0
				? ({} as DataFromColumns<T>)
				: [],
		);
		for (let i = 0; i < data.length; i++) {
			const row = data[i];
			fragement.appendChild(this.renderRow(row));
			if (this.data.length === 0 && Object.keys(row).length > 0) {
				this.data.push(row);
			}
		}
		if (this.summary) {
			fragement.appendChild(<tr class="summary">{this.renderSummary()}</tr>);
		}
		this.el.querySelector('tbody')!.replaceChildren(fragement);
		this.adjustColumnSize();
	}

	private renderSummary(): HTMLTableCellElement[] {
		return (
			this.summary?.map(
				(summary) =>
					(
						<td colSpan={summary.colSpan ?? 1} style={{ textAlign: summary.textAlign ?? 'center' }}>
							{typeof summary.text === 'string' ? summary.text : summary.text.call(this)}
						</td>
					) as HTMLTableCellElement,
			) ?? []
		);
	}

	protected adjustColumnSize() {
		if (!this.el.isConnected) {
			return this.columns;
		}
		const widthes = this.columns.map((c) => c.width);
		if (this.serialNo) {
			widthes.unshift('4em');
		}
		if (this.reorderable && !this.readOnly) {
			widthes.unshift('2em');
		}
		const total = this.el.clientWidth;
		// get all setted with
		let logicTotal = 0;
		let unspecified = 0;
		widthes.forEach((c, idx) => {
			let width = size2px(c, this.el, false);
			if (width === undefined) {
				widthes[idx] = undefined;
				unspecified++;
			} else {
				if (width < MIN_WIDTH) {
					width = MIN_WIDTH;
				}
				widthes[idx] = c;
				logicTotal += width;
			}
		});
		let unspecifyWidth = MIN_WIDTH;
		if (unspecified > 0) {
			unspecifyWidth = Math.floor((total - logicTotal) / unspecified) - 1;
		}
		if (unspecifyWidth < MIN_WIDTH) {
			unspecifyWidth = MIN_WIDTH;
		}
		logicTotal += unspecifyWidth * unspecified;
		// logic with to real with
		const cols: HTMLTableColElement[] = [];
		widthes.forEach((c, idx) => {
			if (c === undefined) {
				widthes[idx] = (unspecifyWidth / logicTotal) * 100 + '%';
			} else {
				widthes[idx] = ((c as number) / logicTotal) * 100 + '%';
			}
			cols.push((<col style={{ width: genWidth(c) }} />) as HTMLTableColElement);
		});
		const colGroup = this.el.querySelector('colgroup');
		colGroup?.replaceChildren(...cols);
		// return columns;
	}

	private getRow(rowNum: number): HTMLTableRowElement | null {
		return this.el.querySelector(`:scope>tbody>tr:nth-child(${rowNum + 2})`);
	}

	rowMoveUp(row: number) {
		if (row > 0) {
			const tr = this.getRow(row);
			if (!tr || !tr.previousElementSibling) return;
			const pos = this.getActivePos();
			const item = this.data.splice(row - 1, 1);
			this.data.splice(row, 0, item[0]);
			tr.previousElementSibling.before(tr);
			// this.renderSerial();
			if (pos) {
				this.activeCell(pos.row - 1, pos.column, true);
			}
		}
		return this;
	}

	rowMoveDown(row: number) {
		if (row < this.data.length - 1) {
			const tr = this.getRow(row);
			if (!tr || !tr.nextElementSibling) return;
			const pos = this.getActivePos();
			const item = this.data.splice(row + 1, 1);
			this.data.splice(row, 0, item[0]);
			tr.nextElementSibling.after(tr);
			// this.renderSerial();
			if (pos) {
				this.activeCell(pos.row + 1, pos.column, true);
			}
		}
		return this;
	}

	activeCell(row: number, column: number, toStart: boolean) {
		window.getSelection()?.removeAllRanges();
		if (this.serialNo) column++;
		if (!this.readOnly && this.reorderable) column++;
		activeTableCell(this.el, row + 1, column, toStart);
		return this;
	}

	getActivePos(): { row: number; column: number } | null {
		const pos = getTableActiveCellPos(this.el);
		if (pos) {
			if (this.serialNo) pos.column--;
			if (this.reorderable && !this.readOnly) pos.column--;
			pos.row--;
			return pos;
		} else return null;
	}

	activePreviousRow() {
		const pos = this.getActivePos();
		if (pos) {
			if (pos.row > 0) {
				this.activeCell(pos.row - 1, pos.column, false);
			}
		}
		return this;
	}

	activeNextRow() {
		const pos = this.getActivePos();
		if (pos) {
			if (pos.row < this.data.length - 1) {
				this.activeCell(pos.row + 1, pos.column, true);
			}
		}
		return this;
	}

	activePreviousCell() {
		const pos = this.getActivePos();
		if (pos) {
			if (pos.column > 0) {
				this.activeCell(pos.row, pos.column - 1, false);
			}
		}
		return this;
	}

	activeNextCell() {
		const pos = this.getActivePos();
		if (pos) {
			if (pos.column < this.columns.length - 1) {
				this.activeCell(pos.row, pos.column + 1, true);
			}
		}
		return this;
	}

	insertRowBefore(cellPos?: { row: number; column: number }) {
		const pos = cellPos ?? this.getActivePos();
		if (pos) {
			this.data.splice(pos.row, 0, {} as DataFromColumns<T>);
			const tr = this.getRow(pos.row);
			if (tr) {
				tr.before(this.renderRow(this.data[pos.row]));
			}
			this.activeCell(pos.row, pos.column, false);
		}
		return this;
	}

	insertRowAfter(cellPos?: { row: number; column: number }) {
		const pos = cellPos ?? this.getActivePos();
		if (pos) {
			this.data.splice(pos.row + 1, 0, {} as DataFromColumns<T>);
			const tr = this.getRow(pos.row);
			if (tr) {
				tr.after(this.renderRow(this.data[pos.row + 1]));
			}
			this.activeCell(pos.row + 1, pos.column, false);
		}
		return this;
	}

	deleteRow(cellPos?: { row: number; column: number }) {
		const pos = cellPos ?? this.getActivePos();
		if (pos) {
			if (this.data.length > 0) {
				this.data.splice(pos.row, 1);
				if (this.data.length === 0) {
					this.renderContent();
				} else {
					const tr = this.getRow(pos.row);
					if (tr) {
						tr.remove();
					}
				}
				if (pos.row > 0) {
					this.activeCell(this.data.length - 1, pos.column, false);
				} else {
					this.activeCell(0, pos.column, false);
				}
			}
		}
		return this;
	}

	emitChange() {
		this.props.onChange?.call(this);
		this.el.dispatchEvent(new CustomEvent('change'));
		if (this.summary) {
			this.el.querySelector('tr.summary')?.replaceChildren(...this.renderSummary());
		}
		return this;
	}

	get summary(): SummaryColumn<T>[] | undefined {
		return this.props.summary;
	}
	setSummary(summary: SummaryColumn<T>[] | undefined): this {
		this.props.summary = summary;
		this.renderContent();
		return this;
	}

	get disableRowAdd(): boolean {
		return this.props.disableRowAdd ?? false;
	}

	setDisableRowAdd(allowAdd: boolean): this {
		this.props.disableRowAdd = allowAdd;
		this.renderContent();
		return this;
	}

	get disableRowDelete(): boolean {
		return this.props.disableRowDelete ?? false;
	}
	setDisableRowDelete(allowDelete: boolean): this {
		this.props.disableRowDelete = allowDelete;
		this.renderContent();
		return this;
	}

	get reorderable(): boolean {
		return this.props.reorderable ?? false;
	}
	setReorderable(reorderable: boolean): this {
		this.props.reorderable = reorderable;
		this.renderContent();
		return this;
	}

	get readOnly(): boolean {
		return this.props.readOnly ?? false;
	}

	setReadOnly(readOnly: boolean): this {
		this.props.readOnly = readOnly;
		this.renderContent();
		return this;
	}

	get serialNo(): boolean {
		return this.props.serialNo ?? false;
	}
	setSerialNo(serialNo: boolean): this {
		this.props.serialNo = serialNo;
		this.renderContent();
		return this;
	}

	get columns(): T {
		return this.props.columns;
	}

	setColumns(columns: T): this {
		this.props.columns = columns;
		this.renderContent();
		return this;
	}

	get data(): DataFromColumns<T>[] {
		if (this.props.data === undefined) {
			this.props.data = [];
		}
		return this.props.data;
	}
	setData(data: DataFromColumns<T>[]): this {
		this.props.data = data;
		this.renderContent();
		return this;
	}

	validate(): boolean {
		let valid = true;
		for (let r = 0; r < this.data.length; r++) {
			const row = this.el.rows[r + 1];
			for (let c = 0; c < this.columns.length; c++) {
				let col = c;
				if (this.serialNo) col++;
				if (!this.readOnly && this.reorderable) col++;
				const cell = row.cells[col];
				const inputCell = this[DATA].get(cell);
				if (inputCell) {
					const v = inputCell.validate();
					valid = valid && v;
				}
			}
		}
		return valid;
	}
}
